package controller.gameloop;

import fiuba.algo3.titiritero.modelo.ObjetoDibujable;
import fiuba.algo3.titiritero.modelo.ObjetoVivo;
import fiuba.algo3.titiritero.modelo.SuperficieDeDibujo;

import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

import business_logic.view.GameView;


public class GameLoop implements Runnable
{
 // GAME_CLOCK : length of the game tick
 // FPS Stands for: Frames Per Seconds, Relative because we use the formula: REAL_FPS = RELATIVE_FPS * GAME_CLOCK
    private static final int DEFAULT_GAME_CLOCK  = 4;
    private static final int DEFAULT_FPS_DIVISOR = 4;
    private Set<ObjetoVivo> m_updateables;
    private List<ObjetoDibujable> m_drawables;
    private boolean m_isRunning;
    private int m_gameClock;
    private int m_fpsDivisor;
    private SuperficieDeDibujo m_drawingSurface;
    private Thread m_thread;
    private int m_fpsClock;
    private List<GameLoopCommand> m_beforeRun;
    private List<GameLoopCommand> m_afterRun;
    private boolean m_hasDraw;
    private boolean m_drawablesAreDirty;
    private boolean m_pauseRequested = false;
    
    private static Comparator<ObjetoDibujable> m_comparator = new Comparator<ObjetoDibujable>()
    {
        @Override
        public int compare(ObjetoDibujable o1, ObjetoDibujable o2)
        {
            Integer zIndex1 = new Integer( ((GameView)o1).getZIndex() );
            Integer zIndex2 = new Integer( ((GameView)o2).getZIndex() );
            
            return zIndex1.compareTo(zIndex2);
        }
    };
    
    public void addBeforeRunListener( GameLoopCommand c )
    {
        this.m_beforeRun.add(c);
    }
    
    public void addAfterRunListener( GameLoopCommand c )
    {
        this.m_afterRun.add(c);
    }
    
    private void executeListeners( List<GameLoopCommand> gameLoopCommands )
    {
        for( GameLoopCommand gameLoopCommand : gameLoopCommands )
        {
            gameLoopCommand.execute(this);
        }
    }
    
    public GameLoop(int gameClock, int fpsDivisor, SuperficieDeDibujo drawingSurface)
    {
        this.m_gameClock      = gameClock;
        this.m_fpsDivisor     = fpsDivisor;
        this.m_drawingSurface = drawingSurface;
        this.m_updateables    = new HashSet<ObjetoVivo>();
        this.m_drawables      = new LinkedList<ObjetoDibujable>();
        this.m_fpsClock       = 0;
        this.m_beforeRun      = new ArrayList<GameLoopCommand>();
        this.m_afterRun       = new ArrayList<GameLoopCommand>();
        this.m_hasDraw        = false;
        this.m_drawablesAreDirty = false;
        
        drawingSurface.actualizar();
    }
    
    public GameLoop(SuperficieDeDibujo drawingSurface)
    {
        this(DEFAULT_GAME_CLOCK,DEFAULT_FPS_DIVISOR, drawingSurface);
    }

    // TODO: Estos métodos (add y delete) son los que probablemente cambien después, como ya lo hablamos
    
    public void add(ObjetoVivo objetoVivo)
    {
        synchronized (m_updateables)
        {
            this.m_updateables.add(objetoVivo);
        }
    }

    public void remove(ObjetoVivo objetoVivo)
    {
        synchronized (m_updateables)
        {
            this.m_updateables.remove(objetoVivo);
        }
    }

    public void add(ObjetoDibujable objetoDibujable)
    {
        synchronized (m_drawables)
        {
            this.m_drawables.add(objetoDibujable);
            m_drawablesAreDirty = true;
        }
    }

    public void remove(ObjetoDibujable objetoDibujable)
    {
        synchronized (m_drawables)
        {
            this.m_drawables.remove(objetoDibujable);
        }
    }
    /**/
    
    private void draw()
    {
        if ( m_drawablesAreDirty )
        {
            Collections.sort(m_drawables, m_comparator);
            m_drawablesAreDirty = false;
        }
        
        for(ObjetoDibujable objetoDibujable : this.m_drawables)
        {
            objetoDibujable.dibujar(this.m_drawingSurface);
        }
        this.m_drawingSurface.actualizar();

    }
    
    @Override
    public void run()
    {   
        while(this.m_isRunning)
        {
            this.executeListeners(this.m_beforeRun);
            
            for(ObjetoVivo objetoVivo : this.m_updateables)
            {
                objetoVivo.vivir();
            }
            
            if ( m_fpsClock == m_fpsDivisor )
            {
                this.draw();
                m_fpsClock=0;
                m_hasDraw = true;
            }
            else
            {
                m_fpsClock++;
                m_hasDraw = false;
            }
            
            try
            {
                Thread.sleep(this.m_gameClock);
            }
            catch (InterruptedException e)
            {
                // TODO log the exception
                e.printStackTrace();
            }
            
            this.executeListeners(this.m_afterRun);
            
            // Si se habia pedido una pausa, se dibuja y luego se pausa
            if ( m_pauseRequested )
            {
                this.draw();
                m_pauseRequested = false;
                //this.m_isRunning = false;
                this.stop();
            }
        }
    }
    
    public boolean hasDraw()
    {
        return m_hasDraw;
    }

    public int getUpdateablesCount()
    {
        return this.m_updateables.size();
    }

    public void start()
    {
        this.m_isRunning = true;
        //this.m_pauseRequested = false;
        this.m_thread = new Thread(this);
        this.m_thread.start();
    }

    public void stop()
    {
        synchronized (this)
        {
            this.m_isRunning = false;
            this.m_thread.interrupt();
        }
    }
    
    public boolean isRunning()
    {
        return this.m_isRunning;
    }

    public void clear()
    {
        synchronized (this)
        {
            this.m_updateables    = new HashSet<ObjetoVivo>();
            this.m_drawables      = new LinkedList<ObjetoDibujable>();
            this.m_hasDraw        = false;
            this.m_drawablesAreDirty = false;
        }
    }
    
    // Es una manera más elegante de hacer stop, ya que espera que se haga un draw loop
    public void pause()
    {
        synchronized (this)
        {
            m_pauseRequested = true;
        }
    }
}
